import { parse } from "@typescript-eslint/typescript-estree";
import { TSESTree } from '@typescript-eslint/typescript-estree';
import { walk } from "estree-walker";
import { Node } from "estree-walker/types/walker";
import fs from "fs-extra";
import path from "path";
import { CommonEnv } from "../common/CommonEnv.js";
import { InterfaceDeclaration, Project, SyntaxKind } from "ts-morph";
import { toolchain } from "../toolchain.js";

export class FilterPuertsReserved {
    private collection: string[] | undefined;

    public async readAll(projectRoot: string, output: string): Promise<string[]> {
        if (this.collection == null) {
            if (fs.existsSync(output)) {
                const content = await fs.readFile(output, 'utf-8');
                this.collection = content.split(/\r?\n+/);
            } else {
                this.collection = [];
            }
            const oldSize = this.collection.length;
            // 收集puerts webgl jslib
            const puertsWebglRoot = toolchain.unity.getPuertsWebglRoot();
            await this.readPuerWebGLJS(path.join(puertsWebglRoot, CommonEnv.PuertsWebGLJSLib), this.collection);
            await this.readPuerDLLMockLibJS(path.join(puertsWebglRoot, CommonEnv.PuertsDLLMockLibJS), this.collection);
            // 收集puerts js库
            const puertsRoot = toolchain.unity.getPuertsCoreRoot();
            await this.readPuerJS(path.join(puertsRoot, CommonEnv.PuertsJSLib), this.collection);
            // 收集puerts Gen
            await this.readPuerGen(path.join(projectRoot, CommonEnv.PuerGen, 'Typing/csharp/index.d.ts'), this.collection);
            // 收集com
            await this.readComLib(path.join(projectRoot, CommonEnv.ComLib), this.collection);
            // 收集项目ts
            await this.readReservedTagged(path.join(projectRoot, CommonEnv.TsSrc), this.collection);
            
            await fs.ensureFile(output);
            this.collection.sort();
            await fs.writeFile(output, this.collection.join('\n'), 'utf-8');
            
            const newSize = this.collection.length;
            if (newSize > oldSize) {
                console.log(`[FilterPuertsReserved] ${newSize - oldSize} more token reserved`/*, this.collection.slice(oldSize + 1, newSize).join(', ')*/);
            } else {
                console.log('[FilterPuertsReserved] no more token reserved.');
            }
        }

        return this.collection;
    }

    private async readPuerWebGLJS(dir: string, collection: string[]): Promise<void> {
        const files = await fs.readdir(dir);
        for (const f of files) {
            const file = path.join(dir, f);
            const fstat = await fs.stat(file);
            if (!fstat.isDirectory() && path.extname(f) === '.jslib') {
                const content = await fs.readFile(file, 'utf-8');
                const results = content.matchAll(/(?<=global\.)\b(\w+)\b/g);
                for (const r of results) {
                    const m = r[1];
                    if (!collection.includes(m)) {
                        collection.push(m);
                    }
                }
            }
        }
    }

    private async readPuerDLLMockLibJS(projRoot: string, collection: string[]): Promise<void> {
        const project = new Project({
            tsConfigFilePath: path.join(projRoot, 'tsconfig.json')
        });
        // library.ts
        const src = project.getSourceFile(path.join(projRoot, 'library.ts'))!;
        const module = src.getModule('PuertsJSEngine')!;
        const ecp = module.getInterface('EngineConstructorParam');
        const uapi = module.getInterface('UnityAPI')!;
        const itfs: InterfaceDeclaration[] = [uapi];
        if (ecp) {
            itfs.push(ecp);
        }
        for (const itf of itfs) {
            const mems = itf.getMembers();
            for (const m of mems) {
                if (m.isKind(SyntaxKind.PropertySignature) || m.isKind(SyntaxKind.MethodSignature)) {
                    const n = m.getName();
                    if (!collection.includes(n)) {
                        console.log(n);
                        collection.push(n);
                    }
                }
            }
        }
    }

    private async readPuerJS(dir: string, collection: string[]): Promise<void> {
        const files = await fs.readdir(dir);
        for (const f of files) {
            const file = path.join(dir, f);
            const fstat = await fs.stat(file);
            if (!fstat.isDirectory() && path.extname(f) === '.mjs') {
                const content = await fs.readFile(file, 'utf-8');
                const results = content.matchAll(/(?<=\b\w+\b\.)\b(\w+)\b/g);
                for (const r of results) {
                    const m = r[1];
                    if (!collection.includes(m)) {
                        collection.push(m);
                    }
                }
            }
        }
    }

    private async readPuerGen(file: string, collection: string[]): Promise<void> {
        const content = await fs.readFile(file, 'utf-8');
        const namespaceResults = content.matchAll(/(?<=namespace )\b([\w\.]+)\b(?= \{)/g);
        for (const r of namespaceResults) {
            const ns = r[1].split('.');
            for (const n of ns) {
                if (!collection.includes(n)) {
                    collection.push(n);
                }
            }
        }
        const classResults = content.matchAll(/(?<=class )\b([\w\$]+)\b(?= )/g);
        for (const c of classResults) {
            const m = c[1];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
        const interfaceResults = content.matchAll(/(?<=interface )\b([\w\$]+)\b(?= )/g);
        for (const i of interfaceResults) {
            const m = i[1];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
        const getterResults = content.matchAll(/(?<=public (?:static )?get )\b(\w+)\b(?=\()/g);
        for (const r of getterResults) {
            const m = r[1];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
        const varResults = content.matchAll(/(?<=public (?:static )?)\b(\w+)\b(?= :)/g);
        for (const r of varResults) {
            const m = r[1];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
        const funcResults = content.matchAll(/(?<=public (?:static )?)\b(\w+)\b(?= \()/g);
        for (const r of funcResults) {
            const m = r[1];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
        const enumResults = content.matchAll(/\b\w+\b(?= = \d+)/g);
        for (const r of enumResults) {
            const m = r[0];
            if (!collection.includes(m)) {
                collection.push(m);
            }
        }
    }

    async readComLib(input: string, collection: string[]): Promise<void> {
        // 目前只支持PropertyDefinition/TSInterfaceDeclaration/TSTypeAliasDeclaration
        const fstat = await fs.stat(input);
        if (fstat.isDirectory()) {
            const files = await fs.readdir(input);
            for (const f of files) {
                const file = path.join(input, f);
                await this.readComLib(file, collection);
            }
        } else if (path.extname(input) === '.ts') {
            const content = await fs.readFile(input, 'utf-8');
            const ast = parse(content, { loc: true });
            let counting: { name: string, members: string[] } | null = null;
            walk(ast as Node, {
                enter: (node, parent, prop, index) => {
                    const alias = node as TSESTree.BaseNode;
                    if (alias.type == TSESTree.AST_NODE_TYPES.TSInterfaceBody) {
                        const name = (parent as unknown as TSESTree.TSInterfaceDeclaration).id.name;
                        counting = { name, members: [] };
                        // console.log('------entering:', name);
                        return;
                    }

                    if (counting != null) {
                        if (alias.type == TSESTree.AST_NODE_TYPES.TSPropertySignature) {
                            const ps = alias as TSESTree.TSPropertySignature;
                            if (ps.computed && ps.key.type == TSESTree.AST_NODE_TYPES.MemberExpression) {
                                // 通常是以枚举作为key的，比如: {[ComLib.Enum]: number}
                                // 此种情形不处理
                            } else {
                                this.expectType(ps.key, TSESTree.AST_NODE_TYPES.Identifier, input, TSESTree.AST_NODE_TYPES.TSPropertySignature);
                                counting.members.push((ps.key as TSESTree.Identifier).name);
                            }
                            // console.log('couting:', (ps.key as TSESTree.Identifier).name);
                        } else if (alias.type == TSESTree.AST_NODE_TYPES.TSMethodSignature) {
                            // 有函数的说明不是纯粹的json数据
                            // const ms = alias as TSESTree.TSMethodSignature;
                            // console.error(`dismiss ${counting.name} cause method encountered: ${(ms.key as TSESTree.Identifier).name}`);
                            counting = null;
                        }
                    }
                }, 
                leave: (node, parent, prop, index) => {
                    const alias = node as TSESTree.BaseNode;
                    if (alias.type == TSESTree.AST_NODE_TYPES.TSInterfaceBody) {
                        if (counting != null) {
                            for (const m of counting.members) {
                                if (!collection.includes(m)) {
                                    collection.push(m);
                                }
                            }
                            counting = null;
                        }
                        // console.log('leaving:', (parent as unknown as TSESTree.TSInterfaceDeclaration).id.name);
                    }
                }
            });
        }
    }

    private async readReservedTagged(input: string, collection: string[]): Promise<void> {
        const fstat = await fs.stat(input);
        if (fstat.isDirectory()) {
            const files = await fs.readdir(input);
            for (const f of files) {
                if (f.startsWith('.')) continue;
                const file = path.join(input, f);
                await this.readReservedTagged(file, collection);
            }
        } else if (path.extname(input) === '.ts') {
            const content = await fs.readFile(input, 'utf-8');
            if (content.includes('@__RESERVED__')) {
                const ast = parse(content, { comment: true, loc: true });
                const reservedLine: number[] = [];
                for (const c of ast.comments) {
                    if (c.value.includes('@__RESERVED__'))
                        reservedLine.push(c.loc.end.line + 1);
                }
                let counting: TSESTree.BaseNode | null = null;
                walk(ast as Node, {
                    enter: (node, parent, prop, index) => {
                        const alias = node as TSESTree.BaseNode;
                        const sline = reservedLine.indexOf(node.loc?.start?.line!);
                        if (sline >= 0) {
                            // console.log('start counting', node.type);
                            reservedLine.splice(sline, 1);
                            if (alias.type == TSESTree.AST_NODE_TYPES.PropertyDefinition) {
                                const pd = alias as TSESTree.PropertyDefinition;
                                this.expectType(pd.key, TSESTree.AST_NODE_TYPES.Identifier, input, TSESTree.AST_NODE_TYPES.PropertyDefinition);
                                const id = pd.key as TSESTree.Identifier;
                                // console.log('couting:', id.name);
                                if (!collection.includes(id.name)) {
                                    collection.push(id.name);
                                }
                            } else {
                                counting = alias;
                            }
                            return;
                        }

                        if (counting != null) {
                            if (alias.type == TSESTree.AST_NODE_TYPES.TSPropertySignature) {
                                const ps = alias as TSESTree.TSPropertySignature;
                                this.expectType(ps.key, TSESTree.AST_NODE_TYPES.Identifier, input, TSESTree.AST_NODE_TYPES.TSPropertySignature);
                                const id = ps.key as TSESTree.Identifier;
                                // console.log('couting:', id.name);
                                if (!collection.includes(id.name)) {
                                    collection.push(id.name);
                                }
                            }
                        }
                    }, 
                    leave: (node, parent, prop, index) => {
                        const alias = node as TSESTree.BaseNode;
                        if (alias == counting) {
                            // console.log('end counting', node.type);
                            counting = null;
                        }
                    }
                });
            }
        }
    }

    private expectType(node: TSESTree.BaseNode, type: TSESTree.AST_NODE_TYPES, input: string, indicateType: TSESTree.AST_NODE_TYPES): void {
        console.assert(node.type == type, `[${TSESTree.AST_NODE_TYPES[indicateType]}] Identifier expected, but got ${TSESTree.AST_NODE_TYPES[node.type]}: ${input}:${this.getNodeLocation(node)}`);
    }

    private getNodeLocation(node: TSESTree.BaseNode): string {
        return `${node.loc?.start.line}:${node.loc?.start.column}`;
    }
}
